package parking;

import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is responsible for simulating entry or exit of cars in the parking. It is a server and implements 
 * the IServeurVoitures interface which makes available the name of the server and a method whereby doors can
 * access to declare themselves to the server. The server can thus know the doors available for it to send
 * an entry or an exit of a vehicle. An important point is that the server duplicates itself as many times as
 * there are doors. In fact, threads are created and launched according to the number of doors.
 * In this way, we can have simultaneously entries of vehicles on various doors. The algorithm can thus show 
 * their efficiencies in times of stresses.
 * It has its own internal counter which is thread safe too as threads may want to access the internal counter
 * for increasing or decreasing the number of spots used within the parking. 
 * @author Sophie and Ronnie
 * @date 9 November 2013
 */
public class ServeurVoitures extends UnicastRemoteObject implements IServeurVoitures, Runnable {

	private static final long serialVersionUID = -131384765520211859L;

	private IDoor[] doors;                  //table representing all the declared doors
	private int nbPortes;                   //number of doors
	private int nbPortesConnues;    //number of doors having declared themselves till now
	private int nbPlaces;                   //number of available spots within the parking
	private CarManager nbCarsIn;   //object representing the internal counter of the server
	private LogManager myLogManager;        //object containing the logger
	private Logger myLogger;                        //logger allowing to write in a file the execution trace of the algorithm

	/**
	 * Constructor for the class. It also allocates memory for the table holding the doors and initializes
	 * the internal counter of the server.
	 * @param nbPortes : number of expected doors
	 * @param nbPlaces : number of available spots within the parking
	 * @throws RemoteException : if RMI registry hasn't been launched correctly
	 */
	public ServeurVoitures(int nbPortes, int nbPlaces, CarManager carManager) throws RemoteException {
		//get the current date and time of the system
		Date today = new Date();
		SimpleDateFormat shortDateFormat = new SimpleDateFormat("ddMMyyyyHHmm");

		this.doors = new IDoor[nbPortes];
		this.nbPortes = nbPortes;
		this.nbPlaces = nbPlaces;
		this.nbPortesConnues = 0;
		this.nbCarsIn = carManager;

		//path and degree for the log file (info for normal outputs, config for debug process)
		this.myLogManager = new LogManager("../logParking/ServeurVoitures/" + "Log_SV_" + shortDateFormat.format(today).toString() + ".log", Level.INFO);
		this.myLogger = this.myLogManager.getlogger();
	}

	/**
	 * Overridden method of the IServeurVoitures interface
	 * Enables doors to register themselves with the server of cars 
	 */
	@Override
	public synchronized void enregistrerPorte(IDoor door) throws RemoteException {
		this.doors[nbPortesConnues] = door;
		this.nbPortesConnues++;
		//whenever a new door has registered itself, a notification is sent to the server
		this.notify();
	}

	/**
	 * Method allowing random generation of cars entering or exiting the parking.
	 * If the parking hasn't any car inside, then only the entry of a car is simulated.
	 * The server assures that the number of cars entering is more than the number of cars exiting.
	 */
	public void generationVoitures() {
		Random rand = new Random(); //random generator
		int type;                                       //0 for an entry, 1 for an exit
		int porte;                                      //id of a door

		//if the number of cars inside the parking is equal to 0,
		//then we can only simulate an entry
		//else randomly choose between 0 and 1
		synchronized (nbCarsIn) {
			if (this.nbCarsIn.getNbCars() == 0) {
				type = 0;
			} else {
				type = rand.nextInt(2);
			}
		}
		
		//execution might be cut but this is done so as to enable other processes to choose
		//a door whenever they access this method
		porte = rand.nextInt(nbPortes);
		IDoor door = doors[porte];

		//case entry of a vehicle
		switch(type) {
		case 0 :
			try {
				
				try {
					//between each simulation step, wait for 5 seconds
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				//door has to be thread safe so as not to mix flow executions among them
				synchronized (door) {
					
					this.myLogger.info("[SERVEUR_VOITURES] : Entree | Porte " + porte);
					door.demandeEntreeParking();
					
					//car has entered, we can now increase the internal counter of the server
					synchronized (nbCarsIn) {
						if (this.nbCarsIn.getNbCars() < this.nbPlaces) {
							this.nbCarsIn.incNbCars();
						}
					}
					
				}

			} catch (RemoteException e) {
				e.printStackTrace();
			}
			break;

			//car exiting
		case 1:
			try {
				try {
					//between each simulation step, wait for 5 seconds
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				//door has to be thread safe so as not to mix flow executions among them
				synchronized (door) {
					
					synchronized (this.nbCarsIn) {
						if (this.nbCarsIn.getNbCars() == 0) {
							return;
						}
					}
					this.myLogger.info("[SERVEUR_VOITURES] : Sortie | Porte " +porte);
					door.demandeSortieParking();
					
					//car exiting, we can decrease the numbers of cars having entered the parking
					synchronized (nbCarsIn) {
						this.nbCarsIn.decNbCars();
					}
				}

			} catch (RemoteException e) {
				e.printStackTrace();
			}
			break;
		}

	}

	/**
	 * Overridden method of the Runnable interface
	 */
	@Override
	public void run() {
		for (;;) {
			this.generationVoitures();
		}
	}

	/**
	 * Method used to recover network address of a distant object
	 * @return network ip @
	 */
	public static String getAddress() {
		try {
			Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();

			while (ifaceList.hasMoreElements()) { 
				List<InterfaceAddress> ifaceAddrList = ifaceList.nextElement().getInterfaceAddresses(); 

				for (InterfaceAddress ifaceAddr : ifaceAddrList) {
					InetAddress addr = ifaceAddr.getAddress();

					if (addr.isSiteLocalAddress()){
						String hostAddr = addr.getHostAddress();
						System.out.println("Host address found: " + hostAddr);
						return hostAddr;
					}
				}
			}
		} catch (SocketException e) {
			e.printStackTrace();
		} 

		System.out.println("Host address NOT found!");
		return "localhost";
	}

	/**
	 * Sets the system property for the ip @ of RMI registry
	 */
	public static void setServerHostname() {
		System.setProperty("java.rmi.server.hostname", getAddress());           
	}

	/**
	 * Server main program
	 * @param args : arg 0 - ip address of RMI registry, arg 1 - RMI port (default 1099), 
	 *                               arg 2 - number of doors, arg 3 - number of spots inside the parking
	 */
	public static void main(String[] args) {
		if (args.length < 4) {
			System.out.println("Il manque des arguments pour le bon fonctionnement du serveur de voitures");
			System.out.println("Usage : java ServeurVoitures <serveur> <port> <nbPortes> <nbPlaces>");
			return;
		}

		String serveur = args[0];
		int port = Integer.parseInt(args[1]);
		int nbPortes = Integer.parseInt(args[2]);
		int nbPlaces = Integer.parseInt(args[3]);
		CarManager carManager = new CarManager(0);
		Thread[] tabThreads = new Thread[nbPortes];

		//sets the system property for locating RMI registry
		setServerHostname();

		try {                   
			//initialization of the server with the corresponding number of doors and spots
			ServeurVoitures servV = new ServeurVoitures(nbPortes, nbPlaces, carManager);
			Registry registry = LocateRegistry.getRegistry(serveur, port);
			registry.rebind(IServeurVoitures.NAME, servV);

			//creation an launching of threads for the doors
			for (int i = 0; i < nbPortes; i++) {
				tabThreads[i] = new Thread(servV);
				System.out.println(tabThreads[i].getName() + " lancé!");
			}

			System.out.println("ServeurVoitures successfully launched!");

			//waiting for all the doors to be connected before starting the simulation
			synchronized(servV) {
				while (servV.nbPortesConnues < servV.nbPortes) {
					servV.wait();
				}
			}

			//running the simulation on all the doors
			for (int i = 0; i < nbPortes; i++) {
				tabThreads[i].start();
			}

		} catch (Exception ex) {
			ex.printStackTrace();
		}

	}

}